/**
 * Licensed under the Apache License Version 2.0.
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package net.interknit.knitter.cg.view;

import java.util.List;

import net.interknit.knitter.cg.CGMain;
import net.interknit.knitter.cg.cfg.CGConfig;
import net.interknit.knitter.cg.cfg.CGConfig.RequestParamType;
import net.interknit.knitter.cg.layout.LayoutElement;
import net.interknit.knitter.cg.layout.TypeElement;
import net.interknit.knitter.cg.model.JavaMethod;
import net.interknit.knitter.cg.model.JavaType;
import net.interknit.knitter.cg.template.CustomTypeDefTemplateHolder;
import net.interknit.knitter.cg.template.ParameterBeanTemplate;
import net.interknit.knitter.cg.template.ParameterBeanTemplateHolder;
import net.interknit.knitter.cg.util.ReflectionHelper;
import net.interknit.knitter.cg.util.Utils;

/**
 *
 */
public class CustomTypeView extends JavaTypeView
{
	public CustomTypeView(JavaType javaType)
	{
		super(javaType);
	}

	@Override
	protected String[] getAdditionalPlaceholders()
	{
		return
		new String[]{
				"STATE_CLASS_NAME",
				"VALUE_CLASS_NAME",
				"METHODS",
		};
	}

	@Override
	public LayoutElement getLayoutElement()
	{
		return new TypeElement(this);
	}

	public String STATE_CLASS_NAME()
	{
		JavaType javaType = getJavaType();

		assertCondition(javaType.getInspector().isCustomType(), "STATE_CLASS_NAME");

		JavaType concreteJavaType = javaType.getConcreteJavaType();

		CustomTypeDefTemplateHolder.getInstance().addCustomTypeDefinition(concreteJavaType);

		return ReflectionHelper.getClassName(concreteJavaType.getTypeClass());
	}

	public String VALUE_CLASS_NAME()
	{
		JavaType javaType = getJavaType();

		assertCondition(javaType.getInspector().isCustomType(), "VALUE_CLASS_NAME");

		JavaType concreteJavaType = javaType.getConcreteJavaType();

		CustomTypeDefTemplateHolder.getInstance().addCustomTypeDefinition(concreteJavaType);

		return ReflectionHelper.getClassName(concreteJavaType.getTypeClass());
	}

	public String METHODS()
	{
		StringBuilder sb = new StringBuilder();

		for(JavaMethod javaMethod : getJavaType().getJavaMethods())
		{
			boolean accessorMethod = javaMethod.isAccessor();

			String methodName = javaMethod.getName();
			String methodId = javaMethod.getId();
			JavaType methodReturnJavaType = javaMethod.getReturnJavaType();
			List<JavaType> methodParameterJavaTypes = javaMethod.getParameterJavaTypes();

			RequestParamType requestParamType = RequestParamType.PATH;
			boolean paramConsolidationRequired = false;
			String[] paramAnnotationNames = null;
			ParameterBeanTemplate paramBeanView = null;

			CGConfig cfg = CGMain.getInstance().getCGConfig();
			String targetClassName =
					ReflectionHelper.getClassName(javaMethod.getTypeClass());

			// if this is not a getter/setter method (i.e., this is a service method)
			if(! accessorMethod)
			{
				String[] parameterClassNames =
					ReflectionHelper.getDisplayClassNames(javaMethod.getMethod().getParameterTypes());

				//
				// append method comments
				//
				String methodComments = cfg.getMethodComments(
						targetClassName, methodName, parameterClassNames);

				if(methodComments != null && ! "".equals(methodComments.trim()))
				{
					sb.append(Utils.formatJavadoc(methodComments, 1));
				}

				//
				// construct @Consumes tag if specified in the configuration.
				// otherwise, leave it (will default to ({"*/*"}))
				//
				String consumes = cfg.getMethodConsumes(
						targetClassName, methodName, parameterClassNames);

				if(consumes != null && ! "".equals(consumes.trim()))
				{
					String[] consumesTokens = consumes.split(",");

					String consumesTag = "@Consumes( {";
					for(int i = 0; i < consumesTokens.length; i++)
					{
						if(i > 0) consumesTag += ",";
						consumesTag += "\"" + consumesTokens[i].trim() + "\"";
					}
					consumesTag += "} )";
					sb.append("\t" + consumesTag).append("\n");
				}

				//
				// construct @Produces tag if specified in the configuration.
				// otherwise, leave it (will default to ({"*/*"}))
				//
				String produces = cfg.getMethodProduces(
						targetClassName, methodName, parameterClassNames);

				if(produces != null && ! "".equals(produces.trim()))
				{
					String[] producesTokens = produces.split(",");

					String producesTag = "@Produces( {";
					for(int i = 0; i < producesTokens.length; i++)
					{
						if(i > 0) producesTag += ",";
						producesTag += "\"" + producesTokens[i].trim() + "\"";
					}
					producesTag += "} )";
					sb.append("\t" + producesTag).append("\n");
				}
				else if(
						methodReturnJavaType.getInspector().isFileType() ||
						methodReturnJavaType.getInspector().isInputStreamType() ||
						methodReturnJavaType.getInspector().isReaderType())
				{
					sb.append("\t" + "@Produces(MediaType.APPLICATION_OCTET_STREAM)").append("\n");
				}

				//
				// find the REST Method if specified in the configuration
				//
				String restMethod = cfg.getMethodRestMethod(
						targetClassName, methodName, parameterClassNames);

				if(restMethod != null)
				{
					// if found, use it
					sb.append("\t" + "@" + restMethod).append("\n");
				}
				else if(methodParameterJavaTypes.isEmpty())
				{
					// if restMethod not found, but if the method takes 0 parameters, default it to GET
					sb.append("\t" + "@GET").append("\n");
				}
				else
				{
					// if not found, default it to POST
					sb.append("\t" + "@POST").append("\n");
				}

				//
				// find the REST Method if specified in the configuration
				//
				String restName = cfg.getMethodRestName(
						targetClassName, methodName, parameterClassNames);

				//
				// create the URL context path based on the method name and optionally,
				// method parameters
				// for example: @Path("/signIn/{p1}/{p2}") // p1 = user name, p2 = password
				// or:          @Path("/deleteEverything") // no parameters
				// use method's name as the resource path base (e.g., 'signIn') if
				// configuration does not have a specific rest-name for this method.
				// otherwise, use the specified name if found.
				//

				String restNameValue = (restName != null? restName : methodId).trim();
				sb.append("\t" + "@Path");
				sb.append("(");
				sb.append("\"").append("/").append("".equals(restNameValue) ? "" : (restNameValue + "/"));

				//
				// method parameters need to be consolidated to a single bean object
				// if any one of the parameter is of a non-basic (text-friendly) type
				//

				// count number of basic type and non basic types from the list of method
				// parameter types

				int numOfTotalTypes = methodParameterJavaTypes.size();
				int numOfNonBasicTypes = 0;

				for(int i = 0; i < methodParameterJavaTypes.size(); i++)
				{
					JavaType parameterJavaType = methodParameterJavaTypes.get(i);

					if(! parameterJavaType.getInspector().isBasicType())
						numOfNonBasicTypes++;
				}

				// if number of non-basic types exceeds 1 where the total parameter count exceeds 1,
				// parameter consolidation is reuired
				paramConsolidationRequired = numOfTotalTypes > 1 && numOfNonBasicTypes > 1;

				requestParamType = cfg.getMethodRequestParamType(
						targetClassName,
						methodName,
						parameterClassNames);

				paramAnnotationNames = cfg.getMethodParameterName(
						targetClassName,
						methodName,
						parameterClassNames);

				//
				// construct the parameterized resource path if parameters do not
				// need to be consolidated into a single bean
				//
				if(! paramConsolidationRequired)
				{
					boolean paramAdded = false;
					for(int i = 0; i < methodParameterJavaTypes.size(); i++)
					{
						if(methodParameterJavaTypes.get(i).getInspector().isBasicType())
						{
							if(requestParamType.equals(RequestParamType.PATH))
							{
								if(paramAdded) sb.append("/");

								String paramAnnotationName = "p" + (i + 1);

								if(paramAnnotationNames != null && i < paramAnnotationNames.length)
								{
									paramAnnotationName = paramAnnotationNames[i];
								}

								sb.append("{").append(paramAnnotationName).append("}");

								paramAdded = true;
							}
						}
					}
				}
				sb.append("\"");
				sb.append(")");
				sb.append("\n");
			}
			else
			{
				String propertyName = javaMethod.getPropertyName();
				String propertyComments = cfg.getTypePropertyComment(
						targetClassName, propertyName);

				String accessorPrefix = null;
				String accessorTag = null;

				if(javaMethod.isGetter() || javaMethod.isQueryMethod())
				{
					accessorPrefix = "Gets";
					accessorTag = "@return";
				}
				else
				{
					accessorPrefix = "Sets";
					accessorTag = "@param arg1";
				}

				if(propertyComments != null && ! propertyComments.equals(""))
				{
					propertyComments = accessorPrefix + " " + propertyComments + "\n";
				}

				propertyComments =
						propertyComments + accessorTag + " " + propertyName + " property of this type";

				sb.append(Utils.formatJavadoc(propertyComments, 1));
			}

			sb.append("\t" + "public ");
			sb.append(JavaTypeViewFactory.create(methodReturnJavaType).ADAPTER_DECLARATION());
			sb.append(" ").append(methodName);
			sb.append("(");

			if(! paramConsolidationRequired)
			{
				for(int i = 0; i < methodParameterJavaTypes.size(); i++)
				{
					JavaType pjt = methodParameterJavaTypes.get(i);
					if(i > 0) sb.append(", ");
					if(! accessorMethod && methodParameterJavaTypes.get(i).getInspector().isBasicType())
					{
						String argAnnotation = requestParamType.annotation();

						String paramAnnotationName = "p" + (i + 1);

						if(paramAnnotationNames != null && i < paramAnnotationNames.length)
						{
							paramAnnotationName = paramAnnotationNames[i];
						}

						sb.append(argAnnotation + "(\"").append(paramAnnotationName).append("\")").append(" ");
					}
					sb.append(JavaTypeViewFactory.create(pjt).ADAPTER_DECLARATION());
					sb.append(" ").append("arg" + (i + 1));
				}
			}
			else
			{
				paramBeanView = ParameterBeanTemplateHolder.getInstance().create(
						javaMethod.getJavaType().getJavaTypeId(false) +
						"_" + methodId + "Args");

				for(int i = 0; i < methodParameterJavaTypes.size(); i++)
				{
					JavaType pjt = methodParameterJavaTypes.get(i);
					paramBeanView.addParameterType(
							JavaTypeViewFactory.create(pjt).ADAPTER_DECLARATION());
				}

				sb.append(paramBeanView.getParameterDeclaration());
			}

			sb.append(")").append("\n");
			sb.append("\t" + "{").append("\n");

			sb.append("\t" + "\t" + "try").append("\n");
			sb.append("\t" + "\t" + "{").append("\n");

			if(paramConsolidationRequired)
			{
				sb.append(paramBeanView.getAssignmentStatements());
			}

			sb.append("\t" + "\t");

			sb.append("\t");

			if(! methodReturnJavaType.getInspector().isVoidType())
			{
				sb.append("return ");
				if(
						methodReturnJavaType.getInspector().isPrimitiveType() ||
						methodReturnJavaType.getInspector().isBasicType() ||
						methodReturnJavaType.getInspector().isJavaTypeIgnored())
				{
					sb.append("");
				}
				else
				{
					sb.append("new ").append(JavaTypeViewFactory.create(methodReturnJavaType).ADAPTER_INSTANTIATION());
				}
				sb.append("(");
			}

			sb.append("defaultState().").append(methodName);
			sb.append("(");

			for(int i = 0; i < methodParameterJavaTypes.size(); i++)
			{
				JavaType pjt = methodParameterJavaTypes.get(i);
				if(i > 0) sb.append(", ");
				JavaTypeView ptv = JavaTypeViewFactory.create(pjt);
				sb.append("arg" + (i + 1) + ptv.UNWRAP_CLAUSE());
			}

			sb.append(")");

			if(! methodReturnJavaType.getInspector().isVoidType())
			{
				sb.append(")");
			}

			sb.append(";").append("\n");

			sb.append("\t" + "\t" + "}").append("\n");
			sb.append("\t" + "\t" + "catch(java.lang.Exception e)").append("\n");
			sb.append("\t" + "\t" + "{").append("\n");
			if(accessorMethod)
			{
				sb.append("\t" + "\t" + "\t" + "throw (e instanceof java.lang.RuntimeException ? ");
				sb.append("((java.lang.RuntimeException) e) : new java.lang.RuntimeException(e));").append("\n");
			}
			else
			{
				sb.append("\t" + "\t" + "\t" + "throw (e instanceof WebApplicationException ? ");
				sb.append("((WebApplicationException) e) : new WebApplicationException(e));").append("\n");
			}
			sb.append("\t" + "\t" + "}").append("\n");

			sb.append("\t" + "}").append("\n");
		}

		return sb.append("\n").toString();
	}
}
